﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;

using NanoMvvm.Collections;
using NanoMvvm.ComponentModel;
using NanoMvvm.Threading;

namespace NanoMvvm
{
    public class DynamicViewModel<TModel> : PropertyObservableBase, IDynamicObject, IDynamicViewModel<TModel>
        where TModel : class
    {
        private readonly DelegateKeyedDictionary<string, PropertyDescriptor> propertyDescriptors;

        private readonly ReaderWriterLockSlim propertyDescriptorsLock;

        private readonly Dictionary<string, List<string>> references;

        public DynamicViewModel(TModel model)
        {
            if (model == null)
            {
                throw new ArgumentNullException("model");
            }

            propertyDescriptorsLock = new ReaderWriterLockSlim();

            references = new Dictionary<string, List<string>>();

            propertyDescriptors = new DelegateKeyedDictionary<string, PropertyDescriptor>(prop => prop.Name);

            WrappedModel = model;

            DynamicViewModelTypeDescriptor.Register(this);
        }

        public TModel WrappedModel { get; private set; }

        public T GetWrappedValue<T>(Expression<Func<TModel, T>> selectorExpression)
        {
            string propName = ExpressionResolver.GetMemberName(selectorExpression);

            if (!propertyDescriptors.Contains(propName))
            {
                throw new ArgumentException("Selected property is not wrapped in the view model.");
            }

            return (T)propertyDescriptors[propName].GetValue(this);
        }

        public void SetWrappedValue<T>(Expression<Func<TModel, T>> selectorExpression, T value)
        {
            string propName = ExpressionResolver.GetMemberName(selectorExpression);

            if (!propertyDescriptors.Contains(propName))
            {
                throw new ArgumentException("Selected property is not wrapped in the view model.");
            }

            propertyDescriptors[propName].SetValue(this, value);
        }

        public void WrapAllProperties()
        {
            var props = TypeDescriptor.GetProperties(WrappedModel);

            using (new ReadLock(propertyDescriptorsLock))
            {
                foreach (PropertyDescriptor prop in props)
                {
                    if (!propertyDescriptors.Contains(prop.Name))
                    {
                        WrapProperty(prop);
                    }
                }
            }
        }

        public void WrapProperty<T>(Expression<Func<TModel, T>> selectorExpression)
        {
            string propertyName = ExpressionResolver.GetMemberName(selectorExpression);

            using (new ReadLock(propertyDescriptorsLock))
            {
                if (propertyDescriptors.Contains(propertyName))
                {
                    throw new ArgumentException("Property with given name already defined on view model.");
                }
            }

            PropertyDescriptor wrappedDescriptor = TypeDescriptor.GetProperties(WrappedModel).Find(propertyName, false);

            WrapProperty(new RelayPropertyDescriptor(wrappedDescriptor));
        }

        public void WrapProperty<T>(
            Expression<Func<TModel, T>> selectorExpression, 
            params Expression<Func<TModel, T>>[] affectedPropertiesSelectors)
        {
            WrapProperty(selectorExpression);

            string propName = ExpressionResolver.GetMemberName(selectorExpression);

            foreach (var propertiesSelector in affectedPropertiesSelectors)
            {
                string prop2Name = ExpressionResolver.GetMemberName(propertiesSelector);

                List<string> list = references.ContainsKey(prop2Name) ? references[prop2Name] : new List<string>();

                list.Add(prop2Name);

                if (!references.ContainsKey(propName))
                {
                    references[propName] = list;
                }
            }
        }

        IEnumerable<PropertyDescriptor> IDynamicObject.GetDynamicPropertyDescriptors()
        {
            return propertyDescriptors;
        }

        IEnumerable<PropertyDescriptor> IDynamicObject.GetDynamicPropertyDescriptors(Attribute[] attributes)
        {
            return propertyDescriptors.Where(pd => pd.Attributes.Contains(attributes));
        }

        void IPropertyChangeSource.RaisePropertyChanged(string propertyName)
        {
            NotifyPropertyChanged(propertyName);
        }

        protected override void NotifyPropertyChanged(string propName)
        {
            base.NotifyPropertyChanged(propName);

            if (references.ContainsKey(propName))
            {
                foreach (var propName2 in references[propName])
                {
                    base.NotifyPropertyChanged(propName2);
                }
            }
        }

        protected void WrapProperty(PropertyDescriptor descriptor)
        {
            if (descriptor == null)
            {
                throw new ArgumentNullException("descriptor");
            }

            var pd = new DynamicViewModelPropertyDescriptor<TModel>(this, descriptor);

            using (new WriteLock(propertyDescriptorsLock))
            {
                propertyDescriptors.Add(pd);
            }
        }
    }
}